using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using EnvDTE;
using EnvDTE80;
using Bordecal.ImportsSorter.Properties;
using System.Threading;

namespace Bordecal.ImportsSorter
{
	/// <summary>
	/// Class used for sorting the imports/using directives in a code file.
	/// </summary>
	internal sealed class FileImportsBlockSorter
	{
		#region Constructors

		/// <summary>
		/// Initializes a new instance of the FileImportsBlockSorter class.
		/// </summary>
		internal FileImportsBlockSorter()
		{
		}

		#endregion

		#region Methods

		/// <summary>
		/// Sorts the imports/using directives for a specified code file.
		/// </summary>
		/// <param name="targetDocument">EnvDTE.Document representing the target file.</param>
		/// <param name="sortingGroups">A one-dimensional string array of namespace prefixes defining the sorting gorups.</param>
		/// <param name="interGroupLineCount">The number of lines to be inserted between sorting groups.</param>
		/// <param name="timer">The <see cref="Timer"/> to use for repositioning the current window after displaying the imports/using block.</param>
		[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "timer")]
		internal void SortImportsBlock(Document targetDocument, string[] sortingGroups, int interGroupLineCount, Timer timer)
		{
			if (targetDocument != null)
			{
				DTE dte = targetDocument.DTE;

				CodeLanguage language = new CodeLanguage(targetDocument.Language);
				dte.UndoContext.Open(TemplatedResources.UndoContextName(language.LanguageType), true);
				try
				{
					// Copy the list of sorting groups into a dictionary sorted in descending order by string length
					// (so that the most complete match for any namespaces will be found first).
					if (sortingGroups == null)
						sortingGroups = new string[] { "System", "Microsoft", string.Empty };
					SortedDictionary<string, int> preparedSortingGroups = new SortedDictionary<string, int>(new DescendingStringLengthComparer());
					for (int i = 0; i < sortingGroups.Length; i++)
					{
						preparedSortingGroups.Add(sortingGroups[i], i);
					}
					if (!preparedSortingGroups.ContainsKey(string.Empty))
						preparedSortingGroups.Add(string.Empty, sortingGroups.Length);

					// Extract the imports/using directive from the target document.
					CodeElements fileElements = targetDocument.ProjectItem.FileCodeModel.CodeElements;
					List<ImportsDirective> importsDirectives = new List<ImportsDirective>();
					TextPoint dummyPoint = null;
					TextPoint startPoint = this.ExtractImportsDirectives(fileElements, importsDirectives, language, preparedSortingGroups, ref dummyPoint);

					// If any imports/using directives were found in the document...
					if (importsDirectives.Count > 0)
					{
						// Write the directives back into the document in sorted order.
						importsDirectives.Sort(new ImportsDirectiveComparer());

						if (startPoint == null)
							startPoint = ((TextDocument)targetDocument.Object("TextDocument")).StartPoint;

						int lastGroup = importsDirectives[0].SortingGroup;

						EditPoint insertionPoint = startPoint.CreateEditPoint();
						foreach (ImportsDirective directive in importsDirectives)
						{
							if (directive.SortingGroup != lastGroup)
							{
								for (int i = 0; i < interGroupLineCount; i++)
								{
									insertionPoint.Insert(Environment.NewLine);
								}

								lastGroup = directive.SortingGroup;
							}

							insertionPoint.Insert(directive.ElementCode);
							insertionPoint.Insert(directive.TrailingComments);
							insertionPoint.Insert(Environment.NewLine);
						}

						// Format the new imports/using block so the tab alignment is correct for its position in the document.
						startPoint.CreateEditPoint().SmartFormat(insertionPoint);

						// Display the new imports/using block, if desired by the user.
						Settings userSettings = Settings.Default;
						if (userSettings.ShowImportsBlockAfterSorting)
						{
							if (userSettings.BlockDisplayDuration > TimeSpan.Zero)
							{
								TimerCallback callback = new TimerCallback(delegate(object state)
									{
										if (dte.ActiveWindow == targetDocument.ActiveWindow)
										{
											((TextSelection)targetDocument.Selection).ActivePoint.TryToShow(vsPaneShowHow.vsPaneShowAsIs, Type.Missing);
										}
									});

								timer = new Timer(callback, null, userSettings.BlockDisplayDuration,
									new TimeSpan(0, 0, 0, 0, -1));
							}

							startPoint.TryToShow(vsPaneShowHow.vsPaneShowAsIs, Type.Missing);
						}
					}
				}
				finally
				{
					dte.UndoContext.Close();
				}
			}
		}

		/// <summary>
		/// Removes the imports/using directives from a CodeElements list and copies their data to a returned collection.
		/// </summary>
		/// <param name="source">The CodeElements list to be searched for imports/using directives.</param>
		/// <param name="target">The list of imports/using directives to which the found directives are to be added.</param>
		/// <param name="language">The code language of the source document.</param>
		/// <param name="preparedSortingGroups">The pre-sorted dictionary of namespace prefixes and positions to be used in determining the sorting position of any given imports/using directive.</param>
		/// <param name="lastImportsStart">The <see cref="TextPoint"/> at which the last import/using directive begins.</param>
		/// <returns>The TextPoint representing the starting point of the first imports/using directive found in the CodeElements collection.</returns>
		private TextPoint ExtractImportsDirectives(CodeElements source, List<ImportsDirective> target, CodeLanguage language, SortedDictionary<string, int> preparedSortingGroups, ref TextPoint lastImportsStart)
		{
			TextPoint startPoint = null;

			bool isVB = (language.LanguageType == LanguageType.VisualBasic);

			foreach (CodeElement element in source)
			{
				if (element.Kind == vsCMElement.vsCMElementImportStmt)
				{
					TextPoint currentStart = element.StartPoint;

					if (startPoint == null) startPoint = currentStart;
					if (lastImportsStart != null) this.DeleteIfWhiteSpace(lastImportsStart, currentStart);
					lastImportsStart = currentStart;

					target.Add(this.PopCodeElement((CodeImport)element, language, preparedSortingGroups));

					if (isVB)
						break;
				}

				switch (language.LanguageType)
				{
					case LanguageType.CSharp:
					case LanguageType.JSharp:
						CodeElements children = element.Children;
						if ((children != null) && (children.Count > 0))
						{
							TextPoint childrenStartPoint = this.ExtractImportsDirectives(children, target, language, preparedSortingGroups, ref lastImportsStart);
							if (startPoint == null)
								startPoint = childrenStartPoint;
						}
						break;
				}
			}

			if (isVB && (startPoint != null))	// VB code model can't identify imports directive following a deleted element.
			{
				this.ExtractImportsDirectives(source, target, language, preparedSortingGroups, ref lastImportsStart);
			}

			return startPoint;
		}

		[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
		private void DeleteIfWhiteSpace(TextPoint startPoint, TextPoint endPoint)
		{
			EditPoint editStart = startPoint.CreateEditPoint();

			string boundedText = editStart.GetText(endPoint);
			boundedText = boundedText.Trim();

			if (boundedText.Length == 0)
			{
				editStart.Delete(endPoint);
				editStart.DeleteWhitespace(vsWhitespaceOptions.vsWhitespaceOptionsHorizontal);
			}
		}

		/// <summary>
		/// Removes a single imports/using directive from a code file.
		/// </summary>
		/// <param name="importElement">The CodeImport element representing the imports/using directive to be removed.</param>
		/// <param name="language">The code language of the source file.</param>
		/// <param name="preparedSortingGroups">The pre-sorted dictionary of namespace prefixes and positions to be used in determining the sorting position of the imports/using directive.</param>
		/// <returns>An ImportsDirective instance containing the data from the removed CodeImport element.</returns>
		[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
		private ImportsDirective PopCodeElement(CodeImport importElement, CodeLanguage language, SortedDictionary<string, int> preparedSortingGroups)
		{
			if (importElement == null)
				throw new ArgumentNullException("importElement");

			EditPoint editPoint = importElement.EndPoint.CreateEditPoint();
			string restOfLine = editPoint.GetText(editPoint.LineLength - editPoint.LineCharOffset + 1);
			if (restOfLine.Trim().StartsWith(language.InlineCommentStart, StringComparison.Ordinal))
			{
				editPoint.Delete(editPoint.LineLength - editPoint.LineCharOffset + 1);
			}
			else
			{
				restOfLine = string.Empty;
			}

			ImportsDirective retVal = new ImportsDirective(importElement, restOfLine, preparedSortingGroups);

			editPoint = importElement.StartPoint.CreateEditPoint();
			editPoint.Delete(importElement.EndPoint);

			editPoint.DeleteWhitespace(vsWhitespaceOptions.vsWhitespaceOptionsHorizontal);
			if (editPoint.AtStartOfLine && editPoint.AtEndOfLine) editPoint.Delete(1);

			return retVal;
		}

		#endregion
	}
}